Pulumiを使って、Next.jsページをホスティングしてみた
こんにちは。データアナリティクス事業本部 サービスソリューション部の北川です。
業務内でpulumiを使用することになったので、今回はNext.jsで作成したページのホスティングを実行してみました。
pulumiとは
Pulumiはインフラ構築・運用をコード化するツール、Infrastructure as Codeの一つです。IaCでは、CloudFormationや、Terraformが有名ですよね。 言語は、TypeScript、JavaScript、Python、Go、C#をサポートしています。
今回、AWS Profileとpulumiの紐付けに関しては、記述しません。 こちらに関して、チームメンバーがエントリしている以下の記事が参考になると思います。
PulumiのGet Started with Google Cloudを試してみた
プロジェクト作成
早速、プロジェクトを作成していきたいと思います
mkdir pulumi-nextjs-project && cd pulumi-nextjs-project
Next.js用のフォルダ作成
初めに、Next.js用のフォルダを作成していきます。
mkdir next && cd next
Next.jsのセットアップ。
$ npx create-next-app --ts .
ローカルで起動
$ yarn dev
Next.jsを使用したことがある方なら、お馴染みの画面が表示されます。
package.jsonを変更
"scripts": { "dev": "next dev", "build": "next build && next export", "start": "next start", "lint": "next lint" },
next exportを使用すると、outディレクトリが生成され、静的ホスティングサービスで使用することができます。
しかし、next/imageはnext exportではサポートされていないらしく、このままyarn buildをするとエラーが出ます。趣旨とずれてしまうので、今回はindex.tsx内のImageタグは削除します。
import type { NextPage } from 'next' import Head from 'next/head' import styles from '../styles/Home.module.css' const Home: NextPage = () => { return ( <div className={styles.container}> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <h1 className={styles.title}> Welcome to <a href="https://nextjs.org">Next.js!</a> </h1> <p className={styles.description}> Get started by editing{' '} <code className={styles.code}>pages/index.tsx</code> </p> <div className={styles.grid}> <a href="https://nextjs.org/docs" className={styles.card}> <h2>Documentation →</h2> <p>Find in-depth information about Next.js features and API.</p> </a> <a href="https://nextjs.org/learn" className={styles.card}> <h2>Learn →</h2> <p>Learn about Next.js in an interactive course with quizzes!</p> </a> <a href="https://github.com/vercel/next.js/tree/canary/examples" className={styles.card} > <h2>Examples →</h2> <p>Discover and deploy boilerplate example Next.js projects.</p> </a> <a href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" className={styles.card} > <h2>Deploy →</h2> <p> Instantly deploy your Next.js site to a public URL with Vercel. </p> </a> </div> </main> <footer className={styles.footer}> <a href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" target="_blank" rel="noopener noreferrer" > Powered by{' '} </a> </footer> </div> ) } export default Home
ビルドを実行します。
$ yarn build
成功すると、nextフォルダの配下にoutディレクトリが作成されます。
Next.js側の設定は以上です。
pulumi-next-projectに戻ります。
$ cd ..
Pulumiの設定
Pulumi側のフォルダを作成
$ mkdir pulumi && cd pulumi
pulumiのセットアップ。今回はtypeScriptで書いていきます。ディレクトリの中が空でないとエラーになります。
$ pulumi new aws-typescript —name pulumi-nextjs-project
プロジェクト名、説明、スタック名、リージョンの記述を求められます。今回、リージョンはap-northeast-1に変更し、他はデフォルトにしました。
作成されたindex.tsを変更し、S3バケットを作成します。
import * as aws from "@pulumi/aws"; import * as pulumi from "@pulumi/pulumi"; import * as mime from "mime"; import * as fs from "fs"; let siteDir = "./frontend/out"; let siteBucket = new aws.s3.Bucket("s3-website-bucket", { tags: { owner: "cm-kitagawa.keita", }, }); const crawlDirectory = (dir: string, f: (_: string) => void) => { const files = fs.readdirSync(dir); for (const file of files) { const filePath = `${dir}/${file}`; const stat = fs.statSync(filePath); if (stat.isDirectory()) { crawlDirectory(filePath, f); } if (stat.isFile()) { f(filePath); } } }; // 指定のpathの中身をS3に同期させます。 crawlDirectory(siteDir, (filePath: string) => { const relativeFilePath = filePath.replace(siteDir + "/", ""); const contentFile = new aws.s3.BucketObject( relativeFilePath, { key: relativeFilePath, acl: "public-read", bucket: siteBucket, contentType: mime.getType(filePath) || undefined, source: new pulumi.asset.FileAsset(filePath), }, { parent: siteBucket, } ); }); export const bucketName = siteBucket.bucket;
スタックをデプロイします。
$ pulumi up
実行した際に作成される、スタックの中身が表示されます。 スタックの更新を実行するか聞かれますので、問題がなければ「yes」を選択します。
Do you want to perform this update? [Use arrows to move, enter to select, type to filter] > yes no details
$ pulumi stack output
で作成されたスタックを出力します。
$ pulumi stack output Current stack outputs (1): OUTPUT VALUE bucketName s3-website-bucket-bf4d365
AWSのコンソール画面でも、S3が作成されていることを確認します。
nextの方で作成したoutフォルダの中身も、アップロードされています。
静的ウェブサイトホスティングを有効化
次にS3のホスティング機能を有効化します。また、オブジェクトを公開して読み取り可能にするため、 バケットポリシーをを作成し、適用させます。 index.tsを変更します。
import * as aws from "@pulumi/aws"; import * as pulumi from "@pulumi/pulumi"; import * as mime from "mime"; import * as fs from "fs"; let siteDir = "../next/out"; let siteBucket = new aws.s3.Bucket("s3-website-bucket", { // ホスティングを有効化 website: { indexDocument: "index.html", }, tags: { owner: "cm-kitagawa.keita", }, }); const crawlDirectory = (dir: string, f: (_: string) => void) => { const files = fs.readdirSync(dir); for (const file of files) { const filePath = `${dir}/${file}`; const stat = fs.statSync(filePath); if (stat.isDirectory()) { crawlDirectory(filePath, f); } if (stat.isFile()) { f(filePath); } } }; crawlDirectory(siteDir, (filePath: string) => { const relativeFilePath = filePath.replace(siteDir + "/", ""); const contentFile = new aws.s3.BucketObject( relativeFilePath, { key: relativeFilePath, acl: "public-read", bucket: siteBucket, contentType: mime.getType(filePath) || undefined, source: new pulumi.asset.FileAsset(filePath), }, { parent: siteBucket, } ); }); const publicReadPolicyForBucket = (bucketName: string) => { return JSON.stringify({ Version: "2012-10-17", Statement: [ { Effect: "Allow", Principal: "*", Action: ["s3:GetObject"], Resource: [ `arn:aws:s3:::${bucketName}/*`, ], }, ], }); } // バケット内のオブジェクトのパブリックアクセスを許可する let bucketPolicy = new aws.s3.BucketPolicy("bucketPolicy", { bucket: siteBucket.bucket, policy: siteBucket.bucket.apply(publicReadPolicyForBucket), }); export const bucketName = siteBucket.bucket; export const websiteUrl = siteBucket.websiteEndpoint;
再度スタックをデプロイします。
$ pulumi up
スタックを出力します。
$ pulumi stack output Current stack outputs (2): OUTPUT VALUE bucketName s3-website-bucket-bf4d365 websiteUrl s3-website-bucket-bf4d365.s3-website-ap-northeast-1.amazonaws.com
websiteUrlの値をコピーし、アクセスします。
Next.jsのWelcomeページが表示されました。
まとめ
今回は、Next.jsで作成したプロジェクトをpulumiを使ってホスティングしてみました。 Pulumiについての記事が少なく、思っていたより苦戦しましたが、おかげで自走力が高められ、いい勉強になりました。
現状は別ページでのリロード時、マッピングが動作しないなどの問題があるので、次回はその辺りも改善できればと思います。
ではまた。